﻿#region File and License Information
/*
<File>
	<License Type="BSD">
		Copyright © 2009 - 2012, Daniel Vaughan. All rights reserved.
	
		This file is part of Calcium (http://calciumsdk.net).

		Redistribution and use in source and binary forms, with or without
		modification, are permitted provided that the following conditions are met:
			* Redistributions of source code must retain the above copyright
			  notice, this list of conditions and the following disclaimer.
			* Redistributions in binary form must reproduce the above copyright
			  notice, this list of conditions and the following disclaimer in the
			  documentation and/or other materials provided with the distribution.
			* Neither the name of the <organization> nor the
			  names of its contributors may be used to endorse or promote products
			  derived from this software without specific prior written permission.

		THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
		ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
		WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
		DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
		DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
		(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
		LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
		ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
		(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
		SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
	</License>
	<Owner Name="Daniel Vaughan" Email="danielvaughan@outcoder.com" />
	<CreationDate>2012-02-18 13:11:43Z</CreationDate>
</File>
*/
#endregion

/* Based on code by Ian Randall, http://ian.geek.nz/ */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;

using DanielVaughan.Collections;
using DanielVaughan.Concurrency;

namespace DanielVaughan.InversionOfControl
{
	/* TODO: [DV] Comment. */

	public class SimpleContainer : IContainer
	{
		readonly Dictionary<Type, FuncDictionary> typeDictionary = new Dictionary<Type, FuncDictionary>();
		readonly Dictionary<Type, Type> singletonTypes = new Dictionary<Type, Type>();
		readonly Dictionary<Type, object> singletonInstances = new Dictionary<Type, object>();
		static readonly string defaultKey = Guid.NewGuid().ToString();
		readonly ReaderWriterLockSlim lockSlim = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

		class FuncDictionary : Dictionary<string, Func<object>>
		{
		}

		public void Register<TFrom, TTo>(bool singleton = false, string key = null) where TTo : TFrom
		{
			Type fromType = typeof(TFrom);
			Type toType = typeof(TTo);
			Register(fromType, toType, singleton, key);
		}

		public void Register(Type fromType, Type toType, bool singleton = false, string key = null)
		{
			key = GetKeyValueOrDefault(key);

			lockSlim.PerformUsingWriteLock(
				delegate
				{
					if (singleton)
					{
						singletonTypes[toType] = fromType;
					}

					FuncDictionary funcDictionary;
					if (!typeDictionary.TryGetValue(fromType, out funcDictionary))
					{
						funcDictionary = new FuncDictionary();
					}

					funcDictionary[key] = () => BuildUp(toType);

					typeDictionary[fromType] = funcDictionary;
				});
		}

		public void Register<TInterface>(TInterface instance, string key = null)
		{
			key = GetKeyValueOrDefault(key);
			Type type = typeof(TInterface);

			lockSlim.PerformUsingWriteLock(
				delegate
				{
					FuncDictionary value;
					if (!typeDictionary.TryGetValue(type, out value))
					{
						value = new FuncDictionary();
					}

					value[key] = () => instance;

					typeDictionary[type] = value;
				});
		}

		public void Register(Type type, object instance, string key = null)
		{
			ArgumentValidator.AssertNotNull(type, "type");
			key = GetKeyValueOrDefault(key);

			lockSlim.PerformUsingWriteLock(
				delegate
				{
					FuncDictionary value;
					if (!typeDictionary.TryGetValue(type, out value))
					{
						value = new FuncDictionary();
					}

					value[key] = () => instance;

					typeDictionary[type] = value;
				});
		}

		public bool IsRegistered<T>(string key = null)
		{
		    return IsRegistered(typeof(T), key);
		}

		public bool IsRegistered(Type fromType, string key = null)
		{
            if (!string.IsNullOrEmpty(key))
            {
				/* Improve performance of this by using a lookup dictionary. */
            	foreach (KeyValuePair<Type, FuncDictionary> pair in typeDictionary)
            	{
					if (!fromType.IsAssignableFrom(pair.Key))
            		{
            			continue;
            		}
            		var funcDictionary = pair.Value;
					if (pair.Value == null)
					{
						continue;
					}
            		foreach (KeyValuePair<string, Func<object>> valuePair in funcDictionary)
            		{
            			if (valuePair.Key == key)
            			{
            				return true;
            			}
            		}
            	}

            	return false;
            }

			return lockSlim.PerformUsingReadLock(
				() => typeDictionary.ContainsKey(fromType));
		}

		public T Resolve<T>(string key = null)
		{
			return (T)ResolveAux(typeof(T), key);
		}

		public object Resolve(Type type, string key = null)
		{
			ArgumentValidator.AssertNotNull(type, "type");
			return ResolveAux(type, key);
		}

		object ResolveAux(Type type, string key = null)
		{
			return lockSlim.PerformUsingReadLock(
				delegate
				{
					object result = ResolveCore(type, key);
					ResolveProperties(result);
					return result;
				});
		}

		public IEnumerable<TFrom> ResolveAll<TFrom>() where TFrom : class
		{
			return lockSlim.PerformUsingReadLock(() => ResolveAllCore<TFrom>());
		}

		public IEnumerable<Object> ResolveAll(Type fromType)
		{
			ArgumentValidator.AssertNotNull(fromType, "fromType");
			return lockSlim.PerformUsingReadLock(() => ResolveAllCore(fromType));
		}

		IEnumerable<Object> ResolveAllCore(Type fromType)
		{
			var list = new List<object>();
			
			FuncDictionary resolveFuncs;
			if (!typeDictionary.TryGetValue(fromType, out resolveFuncs))
			{
				return list;
			}

			if (resolveFuncs == null)
			{
				object builtObject = BuildUp(fromType);
				if (builtObject != null)
				{
					list.Add(builtObject);
				}
			}
			else
			{
				list.AddRange(resolveFuncs.Values.Select(resolveFunc => resolveFunc()));
			}

			return list;
		}

		IEnumerable<TFrom> ResolveAllCore<TFrom>() where TFrom : class
		{
			Type fromType = typeof(TFrom);
			List<TFrom> list = new List<TFrom>();

			FuncDictionary resolveFuncs;
			if (!typeDictionary.TryGetValue(fromType, out resolveFuncs))
			{
				return list;
			}

			if (resolveFuncs == null)
			{
				TFrom builtObject = BuildUp(fromType) as TFrom;
				if (builtObject != null)
				{
					list.Add(builtObject);
				}
			}
			else
			{
				list.AddRange(resolveFuncs.Values.Select(resolveFunc => resolveFunc()));
			}

			return list;
		}

		object ResolveCore(Type type, string key)
		{
			key = GetKeyValueOrDefault(key);

			if (type == null)
			{
				type = ResolveType(key);
			}

			if (type == null)
			{
				throw new ResolutionException("Failed to resolve type for " + key);
			}

			FuncDictionary resolvers = typeDictionary.SingleOrDefault(r => r.Key.Equals(type)).Value;

			if (resolvers == null)
			{
				return BuildUp(type);
			}

			/* If the ResolveProperties method calls through here, it may be searching for a registration per
			 * strongly-typed name that does not exist. In this case, just get the default registration. */
			if (!resolvers.Keys.Contains(key))
			{
				key = GetKeyValueOrDefault(null);
			}

			Func<object> resolveFunc = resolvers.First(
				x => x.Key.Equals(key, StringComparison.CurrentCultureIgnoreCase)).Value;

			return resolveFunc != null ? resolveFunc() : BuildUp(type);
		}

		Type ResolveType(string key)
		{
			Type result = GetTypeFromContainer(key);
			if (result != null)
			{
				return result;
			}

			/*  Not in the container? Try the Assembly. */
			return Assembly.GetExecutingAssembly().GetTypes().SingleOrDefault(t => t.Name == key);
		}

		Type GetTypeFromContainer(string key)
		{
			var matchingResolvers = typeDictionary.Values.Where(r => r.ContainsKey(key));
			var resolver = matchingResolvers.FirstOrDefault();

			if (resolver == null)
			{
				return null;
			}

			KeyValuePair<Type, FuncDictionary> item = typeDictionary.Single(r => r.Value == resolver);
			return item.Key;
		}

		void ResolveProperties(object instance)
		{
			PropertyInfo[] propertyInfoCollection = instance.GetType().GetProperties();

			foreach (PropertyInfo propertyInfo in propertyInfoCollection)
			{
				if (!propertyInfo.GetCustomAttributes(typeof(InjectDependenciesAttribute), false).Any())
				{
					continue;
				}

				string fullPropertyName = string.Format(
					"{0}.{1}", instance.GetType().FullName, propertyInfo.Name);

				object property = Resolve(propertyInfo.PropertyType, fullPropertyName);

				propertyInfo.SetValue(instance, property, null);
			}
		}
		
		object BuildUp(Type type)
		{
			if (type == null)
			{
				throw new ResolutionException("type should not be null.");
			}

			if (singletonTypes.ContainsKey(type))
			{
				object instance;
				if (singletonInstances.TryGetValue(type, out instance))
				{
					return instance;
				}

				instance = Instantiate(type);
				if (instance == null)
				{
					throw new ResolutionException("Failed to instantiate type " + type.Name);
				}

				singletonInstances[type] = instance;
				return instance;
			}

			return Instantiate(type);
		}

		object Instantiate(Type type)
		{
			var constructors = type.GetConstructors();
			ConstructorInfo constructor = (from constructorInfo in constructors
			                  where constructorInfo.GetCustomAttributes(typeof(InjectDependenciesAttribute), false).Any()
			                  select constructorInfo).FirstOrDefault();
			
			if (constructor == null)
			{
				constructor = constructors.OrderByDescending(
					c => c.GetParameters().Length).FirstOrDefault();
			}

			if (constructor == null)
			{
				throw new ResolutionException(
					"Could not locate a constructor for " + type.FullName);
			}

			var constructorParams = new List<object>(constructor.GetParameters().Length);
			foreach (ParameterInfo parameterInfo in constructor.GetParameters())
			{
				object parameter = Resolve(parameterInfo.ParameterType);
				if (parameter == null)
				{
					throw new ResolutionException(
						"Failed to instantiate parameter " + parameterInfo.Name);
				}

				constructorParams.Add(parameter);
			}

			try
			{
				return constructor.Invoke(constructorParams.ToArray());
			}
			catch (Exception ex)
			{
				throw new ResolutionException("Failed to resolve " + type.Name, ex);
			}
		}

		static string GetKeyValueOrDefault(string key)
		{
			if (key != null)
			{
				key = key.Trim();
			}
			return string.IsNullOrEmpty(key) ? defaultKey : key;
		}
	}
}
